Micron Document
πŸŽ–οΈGitΠ―Ρ€Π°πŸŽ–οΈ


Displaying Raw β€’ Download

feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt bd2863243bab6eb213401d949839a2bc74dde7e2 (bd286324) Text, 32.14 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.feature.firmware

Tff7b72import T7ee787kotlinx.serialization.json.Json
Tff7b72import T7ee787org.meshtastic.core.model.DeviceHardware
Tff7b72import T7ee787org.meshtastic.core.model.MaintenanceUf2Manifest
Tff7b72import T7ee787org.meshtastic.core.model.SoftDeviceVariant
Tff7b72import T7ee787kotlin.test.Test
Tff7b72import T7ee787kotlin.test.assertEquals
Tff7b72import T7ee787kotlin.test.assertFalse
Tff7b72import T7ee787kotlin.test.assertNotNull
Tff7b72import T7ee787kotlin.test.assertNull
Tff7b72import T7ee787kotlin.test.assertTrue

T8b949e/**
* Tests the pure availability gate and pinned-asset resolvers.
*
* These are the safety-critical decisions for factory erase, so they live in a pure function with no fakes: the
* dangerous direction (writing a wrong-SoftDevice erase image) cannot be tested on hardware without destroying a
* device, which makes table-driven unit coverage of the refusal logic the only real verification available.
*
* Concrete (not a `Common*Test` base) because nothing here touches `CommonUri`, so it needs no platform subclass and
* cannot silently contribute zero tests.
*/
Tff7b72class T56d364UsbMaintenanceGateTest Tb4b4b4{

T8b949e/**
* The real maintenance-UF2 manifest ([api/data/maintenanceUf2.json] in `meshtastic/api`, embedded verbatim), so
* this test suite keeps exercising the exact board/digest table that ships, not a hand-trimmed fixture that could
* drift from it silently.
*/
Tff7b72private Tff7b72val Te6edf3testManifest Tff7b72=
Te6edf3Json Tb4b4b4{ Te6edf3ignoreUnknownKeys Tff7b72= Tff7b72true Tb4b4b4}
Tb4b4b4.Te6edf3decodeFromStringTff7b72<Te6edf3MaintenanceUf2ManifestTff7b72>Tb4b4b4(
Ta5d6ff"""
Ta5d6ff {
Ta5d6ff"Ta5d6ffmanifestVersionTa5d6ff"Ta5d6ff: 1,
Ta5d6ff"Ta5d6ffotafixReleaseTagTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff0.9.2-OTAFIX2.3-BP1.5Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffotafixBaseTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffhttps://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6fferaseTa5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffnrf52Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ff6.1.1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6fffileNameTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffnrf_erase2.uf2Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3feTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffexpectedFirstTargetAddressTa5d6ff"Ta5d6ff: 155648
},
Ta5d6ff"Ta5d6ff7.3.0Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6fffileNameTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffnrf_erase_sd7_3.uf2Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468fTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffexpectedFirstTargetAddressTa5d6ff"Ta5d6ff: 159744
}
},
Ta5d6ff"Ta5d6ffrp2040Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6fffileNameTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffpico_erase.uf2Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853Ta5d6ff"
Ta5d6ff }
},
Ta5d6ff"Ta5d6ffotafixByBoardIdTa5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffHT-n5262Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffheltec_t114Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19Ta5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffMinewSemi-MX25LE01Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffminewsemi_mx25le01Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffe09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1bTa5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffTRACKER L1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffwio_tracker_l1Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9eTa5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffwiscore_rak4631_boardTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305Ta5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffWisMesh-TagTa5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffwismesh_tagTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758Ta5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-SeeedSenseCAPSolarP1-v1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffsensecap_solar_p1Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5Ta5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-SeeedXiao-v1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffxiao_nrf52840_bleTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67fTa5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-SeeedXiaoSense-v1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffxiao_nrf52840_ble_senseTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6fffc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40daTa5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-T1000-E-v1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6fft1000_eTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4Ta5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-TEcho-v1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6fflilygo_techoTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0Ta5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-ThinkNode-M3-v1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffthinknode_m3Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffbf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708Ta5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-ThinkNodeM1-v1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffthinknode_m1Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffaa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795Ta5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-ThinkNodeM6-v1Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffthinknode_m6Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffaaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4aTa5d6ff"
Ta5d6ff },
Ta5d6ff"Ta5d6ffnRF52840-promicroTa5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffpromicro_nrf52840Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794Ta5d6ff"
Ta5d6ff }
},
Ta5d6ff"Ta5d6ffotafixSupportedTargetsTa5d6ff"Ta5d6ff: [
Ta5d6ff"Ta5d6ffrak4631Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffrak_wismeshtagTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6fft-echoTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffheltec-mesh-node-t114Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffnrf52_promicro_diy_tcxoTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffthinknode_m1Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffthinknode_m3Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffthinknode_m6Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6fftracker-t1000-eTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffseeed_wio_tracker_L1Ta5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffseeed_wio_tracker_L1_einkTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffseeed_solar_nodeTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffseeed_xiao_nrf52840_kitTa5d6ff"
Ta5d6ff ]
}
Ta5d6ff"""
Tb4b4b4.Te6edf3trimIndentTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffnrfTb4b4b4(
Te6edf3variantTb4b4b4: Te6edf3SoftDeviceVariant? Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4,
Te6edf3targetTb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ffrak4631Ta5d6ff"Tb4b4b4,
Te6edf3slugTb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ffRAK4631Ta5d6ff"Tb4b4b4,
Tb4b4b4) Tff7b72= Te6edf3DeviceHardwareTb4b4b4(
Te6edf3hwModelSlug Tff7b72= Te6edf3slugTb4b4b4,
Te6edf3platformioTarget Tff7b72= Te6edf3targetTb4b4b4,
Te6edf3architecture Tff7b72= Ta5d6ff"Ta5d6ffnrf52840Ta5d6ff"Tb4b4b4,
Te6edf3softDeviceVariant Tff7b72= Te6edf3variantTb4b4b4,
Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffrp2040Tb4b4b4(Te6edf3targetTb4b4b4: Tffa657String Tff7b72= Ta5d6ff"Ta5d6ffpicoTa5d6ff"Tb4b4b4) Tff7b72=
Te6edf3DeviceHardwareTb4b4b4(Te6edf3hwModelSlug Tff7b72= Ta5d6ff"Ta5d6ffRPI_PICOTa5d6ff"Tb4b4b4, Te6edf3platformioTarget Tff7b72= Te6edf3targetTb4b4b4, Te6edf3architecture Tff7b72= Ta5d6ff"Ta5d6ffrp2040Ta5d6ff"Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffesp32Tb4b4b4(Tb4b4b4) Tff7b72=
Te6edf3DeviceHardwareTb4b4b4(Te6edf3hwModelSlug Tff7b72= Ta5d6ff"Ta5d6ffHELTEC_V3Ta5d6ff"Tb4b4b4, Te6edf3platformioTarget Tff7b72= Ta5d6ff"Ta5d6ffheltec-v3Ta5d6ff"Tb4b4b4, Te6edf3architecture Tff7b72= Ta5d6ff"Ta5d6ffesp32-s3Ta5d6ff"Tb4b4b4)

T8b949e// ── Architecture and transport gating (R11) ──────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is shown for nrf52840 over usb with a release`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3gate Tff7b72= Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3showTb4b4b4, Ta5d6ff"Ta5d6ffnRF52840 over USB should offer maintenanceTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3eraseRefusalTb4b4b4, Ta5d6ff"Ta5d6ffA resolved SoftDevice must not refuseTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is shown for rp2040 over usb and never refuses on softdevice`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3gate Tff7b72= Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3rp2040Tb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3showTb4b4b4, Ta5d6ff"Ta5d6ffRP2040 over USB should offer maintenanceTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3eraseRefusalTb4b4b4, Ta5d6ff"Ta5d6ffRP2040 has no SoftDevice to resolveTa5d6ff"Tb4b4b4)
Te6edf3assertFalseTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3showBootloaderUpgradeTb4b4b4, Ta5d6ff"Ta5d6ffOTAFIX is nRF-onlyTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/**
* The manifest is now fetched from `resource/maintenanceUf2` rather than compiled in, so a hostile or corrupt row
* is reachable input. A traversal-shaped `fileName` must refuse *that image* and leave the rest of the screen
* working β€” throwing out of a resolver every caller documents as nullable would surface as a blanket
* `FirmwareUpdateState.Error` and hide the maintenance action for every device.
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`an unsafe erase filename refuses the image instead of throwing`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3hostile Tff7b72=
Te6edf3Json Tb4b4b4{ Te6edf3ignoreUnknownKeys Tff7b72= Tff7b72true Tb4b4b4}
Tb4b4b4.Te6edf3decodeFromStringTff7b72<Te6edf3MaintenanceUf2ManifestTff7b72>Tb4b4b4(
Ta5d6ff"""
Ta5d6ff {
Ta5d6ff"Ta5d6fferaseTa5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffnrf52Ta5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ff6.1.1Ta5d6ff"Ta5d6ff: { Ta5d6ff"Ta5d6fffileNameTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff../../etc/passwdTa5d6ff"Ta5d6ff, Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff00Ta5d6ff"Ta5d6ff }
},
Ta5d6ff"Ta5d6ffrp2040Ta5d6ff"Ta5d6ff: { Ta5d6ff"Ta5d6fffileNameTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffsub/dir/pico_erase.uf2Ta5d6ff"Ta5d6ff, Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff00Ta5d6ff"Ta5d6ff }
}
}
Ta5d6ff"""
Tb4b4b4.Te6edf3trimIndentTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Te6edf3assertNullTb4b4b4(Te6edf3eraseUf2ForTb4b4b4(Te6edf3hostileTb4b4b4, Te6edf3nrfTb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA traversal fileName must resolve to null, not throwTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3eraseUf2ForTb4b4b4(Te6edf3hostileTb4b4b4, Te6edf3rp2040Tb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA separator in fileName must resolve to null, not throwTa5d6ff"Tb4b4b4)

Tff7b72val Te6edf3gate Tff7b72= Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3hostileTb4b4b4, Te6edf3rp2040Tb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3UsbMaintenanceRefusalTb4b4b4.Te6edf3MaintenanceDataUnavailableTb4b4b4, Te6edf3gateTb4b4b4.Te6edf3eraseRefusalTb4b4b4)
Tb4b4b4}

T8b949e/** Same contract for the OTAFIX side, whose file name is composed from two manifest-supplied strings. */
Tf0883e@Test
Tff7b72fun Td2a8ff`an unsafe otafix slug or tag refuses the image instead of throwing`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3hostile Tff7b72=
Te6edf3Json Tb4b4b4{ Te6edf3ignoreUnknownKeys Tff7b72= Tff7b72true Tb4b4b4}
Tb4b4b4.Te6edf3decodeFromStringTff7b72<Te6edf3MaintenanceUf2ManifestTff7b72>Tb4b4b4(
Ta5d6ff"""
Ta5d6ff {
Ta5d6ff"Ta5d6ffotafixReleaseTagTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff../../../evilTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffotafixBaseTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffhttps://example.invalid/releasesTa5d6ff"Ta5d6ff,
Ta5d6ff"Ta5d6ffotafixByBoardIdTa5d6ff"Ta5d6ff: {
Ta5d6ff"Ta5d6ffrak4631Ta5d6ff"Ta5d6ff: { Ta5d6ff"Ta5d6ffotafixBoardSlugTa5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ffrak4631Ta5d6ff"Ta5d6ff, Ta5d6ff"Ta5d6ffsha256Ta5d6ff"Ta5d6ff: Ta5d6ff"Ta5d6ff00Ta5d6ff"Ta5d6ff }
}
}
Ta5d6ff"""
Tb4b4b4.Te6edf3trimIndentTb4b4b4(Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Te6edf3assertNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3hostileTb4b4b4, Ta5d6ff"Ta5d6ffrak4631Ta5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA traversal release tag must resolve to null, not throwTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/**
* RP2040 has a UF2 erase path, so an absent image is missing data β€” not an unsupported architecture. The two
* refusals carry different copy ("try again" vs "not possible on this device").
*/
Tf0883e@Test
Tff7b72fun Td2a8ff`rp2040 with no erase data reports missing data rather than unsupported architecture`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3empty Tff7b72= Te6edf3MaintenanceUf2ManifestTb4b4b4(Tb4b4b4)

Tff7b72val Te6edf3gate Tff7b72= Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3emptyTb4b4b4, Te6edf3rp2040Tb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3assertEqualsTb4b4b4(Te6edf3UsbMaintenanceRefusalTb4b4b4.Te6edf3MaintenanceDataUnavailableTb4b4b4, Te6edf3gateTb4b4b4.Te6edf3eraseRefusalTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is hidden for esp32 even over usb`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertFalseTb4b4b4(Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3esp32Tb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4.Te6edf3showTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is hidden for every non-usb transport`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3listOfTb4b4b4(Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3BleTb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3WifiTb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UnknownTb4b4b4)Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3method Tff7b72-Tff7b72>
Te6edf3assertFalseTb4b4b4(
Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3methodTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)Tb4b4b4.Te6edf3showTb4b4b4,
Ta5d6ff"Ta5d6ffMaintenance must not be offered over Tffd700$Te6edf3methodTa5d6ff β€” the flow needs the UF2 mass-storage driveTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`gate is hidden without a release because there would be nothing to reflash`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertFalseTb4b4b4(Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72falseTb4b4b4)Tb4b4b4.Te6edf3showTb4b4b4)
Tb4b4b4}

T8b949e// ── Fail-closed SoftDevice refusal (R4) ──────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`unresolved softdevice shows the action but refuses it`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3gate Tff7b72= Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Te6edf3variant Tff7b72= Tff7b72nullTb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3gateTb4b4b4.Te6edf3showTb4b4b4, Ta5d6ff"Ta5d6ffThe action stays visible so the refusal can be explainedTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3UsbMaintenanceRefusalTb4b4b4.Te6edf3UnknownSoftDeviceTb4b4b4, Te6edf3gateTb4b4b4.Te6edf3eraseRefusalTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`no erase image is resolved for an unresolved softdevice`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(
Te6edf3eraseUf2ForTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Te6edf3variant Tff7b72= Tff7b72nullTb4b4b4)Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffAn unknown variant must never fall back to a default imageTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`each softdevice variant resolves to its own image and target address`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3six Tff7b72= Te6edf3eraseUf2ForTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Te6edf3variant Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3seven Tff7b72= Te6edf3eraseUf2ForTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Te6edf3variant Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4)Tb4b4b4)

Te6edf3assertNotNullTb4b4b4(Te6edf3sixTb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3sevenTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnrf_erase2.uf2Ta5d6ff"Tb4b4b4, Te6edf3sixTb4b4b4.Te6edf3fileNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnrf_erase_sd7_3.uf2Ta5d6ff"Tb4b4b4, Te6edf3sevenTb4b4b4.Te6edf3fileNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Te6edf3x26000LTb4b4b4, Te6edf3sixTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Te6edf3x27000LTb4b4b4, Te6edf3sevenTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3sixTb4b4b4.Te6edf3sha256 Tff7b72!Tff7b72= Te6edf3sevenTb4b4b4.Te6edf3sha256Tb4b4b4, Ta5d6ff"Ta5d6ffThe two variants must not share a digestTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`esp32 resolves no erase image`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3eraseUf2ForTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3esp32Tb4b4b4(Tb4b4b4)Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`rp2040 resolves the pico erase image with no address invariant`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3asset Tff7b72= Te6edf3eraseUf2ForTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3rp2040Tb4b4b4(Tb4b4b4)Tb4b4b4)

Te6edf3assertNotNullTb4b4b4(Te6edf3assetTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffpico_erase.uf2Ta5d6ff"Tb4b4b4, Te6edf3assetTb4b4b4.Te6edf3fileNameTb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3assetTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4, Ta5d6ff"Ta5d6ffRP2040 carries no variant-to-address invariantTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e// ── OTAFIX bootloader image resolution (R5) ──────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`otafix is offered only for supported targets`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertTrueTb4b4b4(
Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Te6edf3target Tff7b72= Ta5d6ff"Ta5d6ffrak4631Ta5d6ff"Tb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3showBootloaderUpgradeTb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(
Te6edf3usbMaintenanceGateTb4b4b4(
Te6edf3testManifestTb4b4b4,
Te6edf3nrfTb4b4b4(Te6edf3target Tff7b72= Ta5d6ff"Ta5d6ffheltec-mesh-node-t114Ta5d6ff"Tb4b4b4)Tb4b4b4,
Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4,
Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4,
Tb4b4b4)
Tb4b4b4.Te6edf3showBootloaderUpgradeTb4b4b4,
Ta5d6ff"Ta5d6ffT114 is on OTAFIX's supported list even though the project names differTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`otafix is not offered for products that merely share a supported build target`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// WISMESH Hub/Tap, Nomadstar Meteor Pro and RAK3401 all build against wiscore_rak4631, and T-Echo Plus/Lite
T8b949e// against t-echo, but OTAFIX ships no bootloader for those products.
Te6edf3listOfTb4b4b4(
Ta5d6ff"Ta5d6ffrak2560Ta5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffrak_wismeshtapTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffrak4631_nomadstar_meteor_proTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6ffrak3401-1wattTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fft-echo-plusTa5d6ff"Tb4b4b4,
Ta5d6ff"Ta5d6fft-echo-liteTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4.Te6edf3forEach Tb4b4b4{ Te6edf3target Tff7b72-Tff7b72>
Te6edf3assertFalseTb4b4b4(
Te6edf3usbMaintenanceGateTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3nrfTb4b4b4(Te6edf3target Tff7b72= Te6edf3targetTb4b4b4)Tb4b4b4, Te6edf3FirmwareUpdateMethodTb4b4b4.Te6edf3UsbTb4b4b4, Te6edf3hasRelease Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4.Te6edf3showBootloaderUpgradeTb4b4b4,
Ta5d6ff"Tffd700$Te6edf3targetTa5d6ff is not an OTAFIX-supported productTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

T8b949e// ── Board-ID resolution: the actual safety gate (R5) ─────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`every shipped otafix image resolves and no two boards share a digest or filename`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(
T79c0ff1T79c0ff4Tb4b4b4,
Te6edf3testManifestTb4b4b4.Te6edf3otafixByBoardIdTb4b4b4.Te6edf3keysTb4b4b4.Te6edf3sizeTb4b4b4,
Ta5d6ff"Tffd700${Te6edf3testManifestTb4b4b4.Te6edf3otafixReleaseTagTffd700}Ta5d6ff ships 14 update imagesTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tff7b72val Te6edf3images Tff7b72=
Te6edf3testManifestTb4b4b4.Te6edf3otafixByBoardIdTb4b4b4.Te6edf3keysTb4b4b4.Te6edf3map Tb4b4b4{
Te6edf3assertNotNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Tffa657itTb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffno image for Tffd700$Te6edf3itTa5d6ff"Tb4b4b4)
Tb4b4b4}
Te6edf3assertEqualsTb4b4b4(Te6edf3imagesTb4b4b4.Te6edf3sizeTb4b4b4, Te6edf3imagesTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3sha256 Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffNo two boards may share a digestTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3imagesTb4b4b4.Te6edf3sizeTb4b4b4, Te6edf3imagesTb4b4b4.Te6edf3map Tb4b4b4{ Tffa657itTb4b4b4.Te6edf3fileName Tb4b4b4}Tb4b4b4.Te6edf3toSetTb4b4b4(Tb4b4b4)Tb4b4b4.Te6edf3sizeTb4b4b4, Ta5d6ff"Ta5d6ffNo two boards may share a filenameTa5d6ff"Tb4b4b4)
Tb4b4b4}

T8b949e/**
* Board-ID -> (release filename, sha256), transcribed from the actual `0.9.2-OTAFIX2.3-BP1.5` release assets
* (downloaded and hashed, not carried over from BP1.4), so a future edit that pairs the right hash with the wrong
* board β€” or vice versa β€” fails here even though it would still pass the uniqueness-only checks above.
*/
Tff7b72private Tff7b72val Te6edf3expectedOtafixAssetsByBoardId Tff7b72=
Te6edf3mapOfTb4b4b4(
Ta5d6ff"Ta5d6ffHT-n5262Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-heltec_t114_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ffae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffMinewSemi-MX25LE01Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-minewsemi_mx25le01_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ffe09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1bTa5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffTRACKER L1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-wio_tracker_l1_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ff70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9eTa5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-wiscore_rak4631_board_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ff8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffWisMesh-TagTa5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-wismesh_tag_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ff96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-SeeedSenseCAPSolarP1-v1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-sensecap_solar_p1_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ff9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-SeeedXiao-v1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-xiao_nrf52840_ble_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ffff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67fTa5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-SeeedXiaoSense-v1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-xiao_nrf52840_ble_sense_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6fffc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40daTa5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-T1000-E-v1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-t1000_e_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ff5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-TEcho-v1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-lilygo_techo_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ff2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-ThinkNode-M3-v1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-thinknode_m3_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ffbf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-ThinkNodeM1-v1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-thinknode_m1_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ffaa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-ThinkNodeM6-v1Ta5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-thinknode_m6_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ffaaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4aTa5d6ff"
Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffnRF52840-promicroTa5d6ff" Te6edf3to
Tb4b4b4(
Ta5d6ff"Ta5d6ffupdate-promicro_nrf52840_bootloader-0.9.2-OTAFIX2.3-BP1.5_nosd.uf2Ta5d6ff" Te6edf3to
Ta5d6ff"Ta5d6ff46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794Ta5d6ff"
Tb4b4b4)Tb4b4b4,
Tb4b4b4)

Tf0883e@Test
Tff7b72fun Td2a8ff`every otafix board id maps to its exact expected release filename and digest`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(
Te6edf3expectedOtafixAssetsByBoardIdTb4b4b4.Te6edf3keysTb4b4b4,
Te6edf3testManifestTb4b4b4.Te6edf3otafixByBoardIdTb4b4b4.Te6edf3keysTb4b4b4,
Ta5d6ff"Ta5d6ffThis test's expectation table and the shipped board-id map have drifted apartTa5d6ff"Tb4b4b4,
Tb4b4b4)
Te6edf3expectedOtafixAssetsByBoardIdTb4b4b4.Te6edf3forEach Tb4b4b4{ Tb4b4b4(Te6edf3boardIdTb4b4b4, Te6edf3expectedTb4b4b4) Tff7b72-Tff7b72>
Tff7b72val Tb4b4b4(Te6edf3expectedFileNameTb4b4b4, Te6edf3expectedSha256Tb4b4b4) Tff7b72= Te6edf3expected
Tff7b72val Te6edf3image Tff7b72= Te6edf3assertNotNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3boardIdTb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffno image for Tffd700$Te6edf3boardIdTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3expectedFileNameTb4b4b4, Te6edf3imageTb4b4b4.Te6edf3fileNameTb4b4b4, Ta5d6ff"Tffd700$Te6edf3boardIdTa5d6ff: wrong release filenameTa5d6ff"Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3expectedSha256Tb4b4b4, Te6edf3imageTb4b4b4.Te6edf3sha256Tb4b4b4, Ta5d6ff"Tffd700$Te6edf3boardIdTa5d6ff: wrong digestTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`board id selects the image rather than the build target`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3rak Tff7b72= Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4)
Tff7b72val Te6edf3techo Tff7b72= Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Ta5d6ff"Ta5d6ffnRF52840-TEcho-v1Ta5d6ff"Tb4b4b4)

Te6edf3assertNotNullTb4b4b4(Te6edf3rakTb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3techoTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3rakTb4b4b4.Te6edf3fileNameTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffwiscore_rak4631_boardTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3techoTb4b4b4.Te6edf3fileNameTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6fflilygo_techoTa5d6ff"Tb4b4b4)Tb4b4b4)
T8b949e// Both boards report USB 239A/0029 in bootloader mode, so USB identity could not have told them apart.
Te6edf3assertTrueTb4b4b4(Te6edf3rakTb4b4b4.Te6edf3sha256 Tff7b72!Tff7b72= Te6edf3techoTb4b4b4.Te6edf3sha256Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`xiao sense is distinguished from plain xiao only by board id`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3plain Tff7b72= Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Ta5d6ff"Ta5d6ffnRF52840-SeeedXiao-v1Ta5d6ff"Tb4b4b4)
Tff7b72val Te6edf3sense Tff7b72= Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Ta5d6ff"Ta5d6ffnRF52840-SeeedXiaoSense-v1Ta5d6ff"Tb4b4b4)

Te6edf3assertNotNullTb4b4b4(Te6edf3plainTb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3senseTb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3plainTb4b4b4.Te6edf3fileNameTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffxiao_nrf52840_ble_bootloaderTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3senseTb4b4b4.Te6edf3fileNameTb4b4b4.Te6edf3containsTb4b4b4(Ta5d6ff"Ta5d6ffxiao_nrf52840_ble_senseTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3plainTb4b4b4.Te6edf3sha256 Tff7b72!Tff7b72= Te6edf3senseTb4b4b4.Te6edf3sha256Tb4b4b4, Ta5d6ff"Ta5d6ffOTAFIX's README warns these must not be interchangedTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`an unrecognized board id refuses rather than falling back`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Ta5d6ff"Ta5d6ffSomeOtherBoard-v9Ta5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Ta5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`board id is parsed from an INFO_UF2 payload and tolerates surrounding lines`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3info Tff7b72=
Ta5d6ff"Ta5d6ffUF2 Bootloader 0.9.2-OTAFIX2.2-BP1.3Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffModel: WisBlock RAK4631 BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBoard-ID: WisBlock-RAK4631-BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDate: Apr 13 2026Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"

Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3infoTb4b4b4)Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3infoTb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Tffa657itTb4b4b4) Tb4b4b4}Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`a payload without a board id line yields null`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Ta5d6ff"Ta5d6ffUF2 Bootloader 0.2.6Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnModel: SomethingTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Ta5d6ff"Ta5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA volume with no INFO_UF2.TXT is not a UF2 bootloader driveTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`board id tolerates leading whitespace on its line just like the softdevice line`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Ta5d6ff"Ta5d6ff Board-ID: WisBlock-RAK4631-BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e// ── SoftDevice read from the drive: the authoritative gate (R4) ───────────

T8b949e/** Verbatim `INFO_UF2.TXT` from a stock Seeed Wio Tracker L1 (hwModel 99), captured 2026-07-30. */
Tff7b72private Tff7b72val Te6edf3seeedL1Info Tff7b72=
Ta5d6ff"Ta5d6ffUF2 Bootloader 0.9.2-dirty lib/nrfx (v2.0.0) lib/tinyusb (0.12.0-145-g9775e7691) Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6fflib/uf2 (remotes/origin/configupdate-9-gadbb8c7)Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffModel: Seeed TRACKER L1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBoard-ID: TRACKER L1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDate: May 15 2025Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffSoftDevice: S140 7.3.0Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"

Tf0883e@Test
Tff7b72fun Td2a8ff`softdevice and board id are both read from a real stock bootloader payload`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Te6edf3seeedL1InfoTb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffTRACKER L1Ta5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3seeedL1InfoTb4b4b4)Tb4b4b4)
T8b949e// The stock Seeed bootloader reports the same Board-ID as OTAFIX's build for this board.
Te6edf3assertNotNullTb4b4b4(Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3seeedL1InfoTb4b4b4)Tff7b72!!Tb4b4b4)Tb4b4b4)
Tb4b4b4}

T8b949e/** Verbatim `INFO_UF2.TXT` from a RAK4631 running OTAFIX 2.2-BP1.3 (hwModel 9), captured 2026-07-30. */
Tff7b72private Tff7b72val Te6edf3rak4631OtafixInfo Tff7b72=
Ta5d6ff"Ta5d6ffUF2 Bootloader 0.9.2-OTAFIX2.2-BP1.3 lib/nrfx (v2.0.0) lib/tinyusb (0.12.0-145-g9775e7691) Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6fflib/uf2 (remotes/origin/configupdate-9-gadbb8c7)Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffModel: WisBlock RAK4631 BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBoard-ID: WisBlock-RAK4631-BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDate: Apr 13 2026Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffSoftDevice: S140 6.1.1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"

T8b949e/**
* Verbatim `INFO_UF2.TXT` from a RAK4631 on a stock 0.4.3 bootloader (May 2023), captured 2026-07-30.
*
* Two things this vintage proves: the `SoftDevice:` line goes back at least this far, so the bundled-map fallback
* is belt-and-braces rather than the common path; and older bootloaders emit an extra `Ver:` line that 0.9.x
* dropped, which the line-scanning parser must tolerate.
*/
Tff7b72private Tff7b72val Te6edf3rak4631StockInfo Tff7b72=
Ta5d6ff"Ta5d6ffUF2 Bootloader 0.4.3Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffModel: WisBlock RAK4631 BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffBoard-ID: WisBlock-RAK4631-BoardTa5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffDate: May 20 2023Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffVer: 0.4.3Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ffSoftDevice: S140 6.1.1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"

Tf0883e@Test
Tff7b72fun Td2a8ff`an old bootloader vintage still reports its softdevice and board id`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Te6edf3rak4631StockInfoTb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631StockInfoTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`board id is stable across bootloader vintages for the same board`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// A stock 0.4.3 RAK and an OTAFIX 2.2 RAK report the same Board-ID, so the OTAFIX veto resolves correctly on a
T8b949e// device that has never been upgraded β€” the case that decides whether the upgrade is offerable at all.
Te6edf3assertEqualsTb4b4b4(Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631StockInfoTb4b4b4)Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631OtafixInfoTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`both variants are parsed from real captured bootloader payloads`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// The two sides of the split, each from hardware: an OTAFIX RAK4631 and a stock Seeed L1.
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Te6edf3rak4631OtafixInfoTb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Te6edf3seeedL1InfoTb4b4b4)Tb4b4b4)

Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffWisBlock-RAK4631-BoardTa5d6ff"Tb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631OtafixInfoTb4b4b4)Tb4b4b4)
Te6edf3assertNotNullTb4b4b4(
Te6edf3otafixUf2ForBoardIdTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3parseUf2BoardIdTb4b4b4(Te6edf3rak4631OtafixInfoTb4b4b4)Tff7b72!!Tb4b4b4)Tb4b4b4,
Ta5d6ff"Ta5d6ffAn OTAFIX-flashed device must still resolve its own image, so re-running the upgrade is idempotentTa5d6ff"Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`softdevice line is parsed for both shipped variants`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: S140 6.1.1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4, Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: S140 7.3.0Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`each erase image targets the app start of the softdevice it is linked for`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// Verified against hardware 2026-07-30: on a 6.1.1 RAK4631 the app vector table sits at 0x26000
T8b949e// (sp=0x20040000, top of nRF52840 RAM), and 0x27000 is mid-application. On a 7.3.0 device the app starts at
T8b949e// 0x27000, which makes 0x26000 the SoftDevice's last page β€” the direction that corrupts.
Tff7b72val Te6edf3six Tff7b72= Te6edf3assertNotNullTb4b4b4(Te6edf3eraseUf2ForVariantTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4)Tb4b4b4)
Tff7b72val Te6edf3seven Tff7b72= Te6edf3assertNotNullTb4b4b4(Te6edf3eraseUf2ForVariantTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4)Tb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Te6edf3x26000LTb4b4b4, Te6edf3sixTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Te6edf3x27000LTb4b4b4, Te6edf3sevenTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`unsupported softdevice ids and absent lines yield null`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: S132 7.3.0Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffS132 is not an nRF52840 SoftDeviceTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: S140 9.9.9Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffNo erase image exists for an unknown versionTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffSoftDevice: Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffThe bootloader omits the value when no SD is installedTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3parseUf2SoftDeviceTb4b4b4(Ta5d6ff"Ta5d6ffBoard-ID: TRACKER L1Ta5d6ff\Ta5d6ffrTa5d6ff\Ta5d6ffnTa5d6ff"Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffAbsent lineTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`the drive outranks the bundled map`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// The map says 6.1.1, the device says 7.3.0 with nothing else to go on β€” but they disagree, so refuse.
Tff7b72val Te6edf3conflict Tff7b72=
Te6edf3resolveNrfEraseImageTb4b4b4(
Te6edf3testManifestTb4b4b4,
Te6edf3mapped Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4,
Te6edf3reportedFromDrive Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4,
Tb4b4b4)
Te6edf3assertTrueTb4b4b4(Te6edf3conflict Tff7b72is Te6edf3EraseImageResolutionTb4b4b4.Te6edf3ConflictTb4b4b4, Ta5d6ff"Ta5d6ffDisagreement must refuse, not pick a sideTa5d6ff"Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`agreement resolves to the reported variant's image`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3resolved Tff7b72=
Te6edf3resolveNrfEraseImageTb4b4b4(
Te6edf3testManifestTb4b4b4,
Te6edf3mapped Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4,
Te6edf3reportedFromDrive Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_7_3_0Tb4b4b4,
Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resolved Tff7b72is Te6edf3EraseImageResolutionTb4b4b4.Te6edf3ResolvedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnrf_erase_sd7_3.uf2Ta5d6ff"Tb4b4b4, Te6edf3resolvedTb4b4b4.Te6edf3assetTb4b4b4.Te6edf3fileNameTb4b4b4)
Te6edf3assertEqualsTb4b4b4(T79c0ff0Te6edf3x27000LTb4b4b4, Te6edf3resolvedTb4b4b4.Te6edf3assetTb4b4b4.Te6edf3expectedFirstTargetAddressTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`an old bootloader with no softdevice line falls back to the bundled map`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3resolved Tff7b72=
Te6edf3resolveNrfEraseImageTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3mapped Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3reportedFromDrive Tff7b72= Tff7b72nullTb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resolved Tff7b72is Te6edf3EraseImageResolutionTb4b4b4.Te6edf3ResolvedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Ta5d6ff"Ta5d6ffnrf_erase2.uf2Ta5d6ff"Tb4b4b4, Te6edf3resolvedTb4b4b4.Te6edf3assetTb4b4b4.Te6edf3fileNameTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`a drive report rescues an unmapped model`Tb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// THINKNODE_M8 has no firmware variant on master and so no map row; the drive can still answer.
Tff7b72val Te6edf3resolved Tff7b72=
Te6edf3resolveNrfEraseImageTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3mapped Tff7b72= Tff7b72nullTb4b4b4, Te6edf3reportedFromDrive Tff7b72= Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4)

Te6edf3assertTrueTb4b4b4(Te6edf3resolved Tff7b72is Te6edf3EraseImageResolutionTb4b4b4.Te6edf3ResolvedTb4b4b4)
Te6edf3assertEqualsTb4b4b4(Te6edf3SoftDeviceVariantTb4b4b4.Te6edf3S140_6_1_1Tb4b4b4, Te6edf3resolvedTb4b4b4.Te6edf3variantTb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`neither source resolving means unresolved`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertEqualsTb4b4b4(
Te6edf3EraseImageResolutionTb4b4b4.Te6edf3UnresolvedTb4b4b4,
Te6edf3resolveNrfEraseImageTb4b4b4(Te6edf3testManifestTb4b4b4, Te6edf3mapped Tff7b72= Tff7b72nullTb4b4b4, Te6edf3reportedFromDrive Tff7b72= Tff7b72nullTb4b4b4)Tb4b4b4,
Tb4b4b4)
Tb4b4b4}

T8b949e// ── UF2 header parsing (R8) ──────────────────────────────────────────────

Tf0883e@Test
Tff7b72fun Td2a8ff`uf2 target address is read from the first block`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3block Tff7b72= Te6edf3ByteArrayTb4b4b4(Te6edf3UF2_BLOCK_BYTESTb4b4b4)
Te6edf3listOfTb4b4b4(T79c0ff0Te6edf3x55Tb4b4b4, T79c0ff0Te6edf3x46Tb4b4b4, T79c0ff0Te6edf3x32Tb4b4b4, T79c0ff0Te6edf3x0ATb4b4b4)Tb4b4b4.Te6edf3forEachIndexed Tb4b4b4{ Te6edf3iTb4b4b4, Te6edf3b Tff7b72-Tff7b72> Te6edf3blockTff7b72[Te6edf3iTff7b72] Tff7b72= Te6edf3bTb4b4b4.Te6edf3toByteTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3blockTff7b72[Te6edf3UF2_TARGET_ADDR_OFFSETTff7b72] Tff7b72= T79c0ff0Te6edf3x00
Te6edf3blockTff7b72[Te6edf3UF2_TARGET_ADDR_OFFSET Tff7b72+ T79c0ff1Tff7b72] Tff7b72= T79c0ff0Te6edf3x70
Te6edf3blockTff7b72[Te6edf3UF2_TARGET_ADDR_OFFSET Tff7b72+ T79c0ff2Tff7b72] Tff7b72= T79c0ff0Te6edf3x02
Te6edf3blockTff7b72[Te6edf3UF2_TARGET_ADDR_OFFSET Tff7b72+ T79c0ff3Tff7b72] Tff7b72= T79c0ff0Te6edf3x00

Te6edf3assertEqualsTb4b4b4(T79c0ff0Te6edf3x27000LTb4b4b4, Te6edf3uf2FirstTargetAddressTb4b4b4(Te6edf3blockTb4b4b4)Tb4b4b4)
Tb4b4b4}

Tf0883e@Test
Tff7b72fun Td2a8ff`non-uf2 payloads yield no target address`Tb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3assertNullTb4b4b4(Te6edf3uf2FirstTargetAddressTb4b4b4(Te6edf3ByteArrayTb4b4b4(Te6edf3UF2_BLOCK_BYTESTb4b4b4)Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffZeroed bytes carry no UF2 magicTa5d6ff"Tb4b4b4)
Te6edf3assertNullTb4b4b4(Te6edf3uf2FirstTargetAddressTb4b4b4(Te6edf3ByteArrayTb4b4b4(T79c0ff1T79c0ff6Tb4b4b4)Tb4b4b4)Tb4b4b4, Ta5d6ff"Ta5d6ffA short payload cannot hold a UF2 blockTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.13s